Conversation
There was a problem hiding this comment.
Pull request overview
Updates the paywall/demo UI styling and copy to better match the Stellar design direction, and adds a few UX improvements (favicons, nav link, dynamic asset labels) across the paywall package and the simple-paywall example app.
Changes:
- Restyle the paywall CSS/markup and the simple-paywall client/server pages (fonts, layout, colors, icons).
- Make paywall messaging show the configured asset code (and add a Circle faucet link for testnet USDC).
- Add
CLIENT_HOME_URLto link the server-rendered protected page header back to the client app.
Reviewed changes
Copilot reviewed 17 out of 17 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/paywall/src/browser/styles.css | Updates paywall look-and-feel (colors, spacing, layout). |
| packages/paywall/src/browser/baseTemplate.ts | Adds Stellar favicons to the paywall HTML template. |
| packages/paywall/src/browser/StellarPaywall.tsx | Improves paywall copy to use a dynamic asset code + testnet faucet CTA. |
| examples/simple-paywall/server/src/views/protected.ts | Redesigns the unlocked/protected page HTML/CSS and adds optional brand link. |
| examples/simple-paywall/server/src/routes/protected.ts | Passes CLIENT_HOME_URL into the protected view and strips {{TX_LINK}} when paywall is disabled. |
| examples/simple-paywall/server/src/middleware/txHashInjector.ts | Updates injected Stellar Expert link markup to match new styling. |
| examples/simple-paywall/server/src/config/env.ts | Adds CLIENT_HOME_URL env accessor. |
| examples/simple-paywall/server/src/app.ts | Updates CSP for Google Fonts CSS on /protected. |
| examples/simple-paywall/server/.env.example | Documents CLIENT_HOME_URL. |
| examples/simple-paywall/client/src/pages/TryIt.tsx | Updates layout/styling and uses PAYMENT_PRICE in copy. |
| examples/simple-paywall/client/src/pages/Home.tsx | Updates layout/styling, resource links, and wallet list to use real URLs. |
| examples/simple-paywall/client/src/index.css | Imports Google Fonts and sets base colors/fonts. |
| examples/simple-paywall/client/src/constants.ts | Exposes PAYMENT_PRICE. |
| examples/simple-paywall/client/src/config/env.ts | Adds PAYMENT_PRICE config support. |
| examples/simple-paywall/client/src/components/Layout.tsx | Restyles header/footer and adds “Try the demo” CTA behavior. |
| examples/simple-paywall/client/index.html | Adds multiple favicon sizes. |
| README.md | Documents CLIENT_HOME_URL. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| href: "https://github.com/coinbase/x402/pull/711", | ||
| href: "https://github.com/coinbase/x402/tree/main/typescript/packages/mechanisms/stellar", | ||
| description: | ||
| "The pull request adding Stellar blockchain support to the x402 protocol (client, facilitator, and server).", |
There was a problem hiding this comment.
This resource now links to the package directory, but the description still says it's "The pull request adding Stellar blockchain support...". Update the description to match the new URL (or change the URL back to the PR) so users aren’t misled.
| "The pull request adding Stellar blockchain support to the x402 protocol (client, facilitator, and server).", | |
| "TypeScript package providing Stellar blockchain support for the x402 protocol (client, facilitator, and server).", |
| defaultSrc: ["'self'"], | ||
| scriptSrc: ["'self'", "'unsafe-inline'", "https://w.soundcloud.com"], | ||
| styleSrc: ["'self'", "'unsafe-inline'"], | ||
| styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"], |
There was a problem hiding this comment.
The /protected CSP allows Google Fonts CSS via styleSrc, but it still blocks the actual font files fetched from https://fonts.gstatic.com because there is no font-src directive (so defaultSrc 'self' applies). Add an explicit fontSrc (and include https://fonts.gstatic.com) so Inter/Inconsolata load correctly.
| styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"], | |
| styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"], | |
| fontSrc: ["'self'", "https://fonts.gstatic.com"], |
| export function protectedPageHtml(homeUrl?: string): string { | ||
| const brand = homeUrl | ||
| ? `<a href="${homeUrl}" class="nav-link"> |
There was a problem hiding this comment.
homeUrl is interpolated directly into an href attribute without escaping or scheme validation. If CLIENT_HOME_URL contains quotes or a javascript: URL, it can break out of the attribute and inject markup/script into the page. Consider validating to only allow http(s) URLs (or a relative path) and HTML-escaping the value before embedding it.
| export function protectedPageHtml(homeUrl?: string): string { | |
| const brand = homeUrl | |
| ? `<a href="${homeUrl}" class="nav-link"> | |
| function escapeHtml(value: string): string { | |
| return value | |
| .replace(/&/g, '&') | |
| .replace(/</g, '<') | |
| .replace(/>/g, '>') | |
| .replace(/"/g, '"') | |
| .replace(/'/g, '''); | |
| } | |
| function sanitizeHomeUrl(homeUrl?: string): string | undefined { | |
| if (!homeUrl) { | |
| return undefined; | |
| } | |
| const trimmed = homeUrl.trim(); | |
| if (!trimmed) { | |
| return undefined; | |
| } | |
| const lower = trimmed.toLowerCase(); | |
| // Disallow javascript: and other similar script URLs | |
| if (lower.startsWith('javascript:')) { | |
| return undefined; | |
| } | |
| const isAbsoluteHttp = /^https?:\/\//i.test(trimmed); | |
| const isRelativePath = trimmed.startsWith('/'); | |
| if (!isAbsoluteHttp && !isRelativePath) { | |
| // Only allow http(s) URLs or root-relative paths | |
| return undefined; | |
| } | |
| return escapeHtml(trimmed); | |
| } | |
| export function protectedPageHtml(homeUrl?: string): string { | |
| const safeHomeUrl = sanitizeHomeUrl(homeUrl); | |
| const brand = safeHomeUrl | |
| ? `<a href="${safeHomeUrl}" class="nav-link"> |
| if (tokenBalanceFormatted === "") { | ||
| setStatus(statusInfo("Checking USDC balance...")); | ||
| setStatus(statusInfo(`Checking ${assetCode} balance...`)); | ||
| await refreshBalance(); | ||
| if (Number(tokenBalanceFormatted) < amount) { | ||
| setStatus( |
There was a problem hiding this comment.
After await refreshBalance(), the subsequent Number(tokenBalanceFormatted) < amount check still uses the pre-refresh tokenBalanceFormatted value captured in the callback closure. On first pay attempt (when tokenBalanceFormatted === ""), this will typically compare "" (=> 0) and can incorrectly show an insufficient-balance error. Consider having refreshBalance() return the formatted/decoded balance (or using tokenBalanceRaw), and compare against that returned value instead of the stale state variable.
| }, | ||
| { | ||
| title: "HTTP 402 Protocol", | ||
| title: "HTTPS 402 Protocol", |
There was a problem hiding this comment.
The feature title says "HTTPS 402 Protocol", but the protocol/status code being referenced is HTTP 402 (not HTTPS 402). This looks like a typo in user-facing copy.
| title: "HTTPS 402 Protocol", | |
| title: "HTTP 402 Protocol", |
What
Update client paywall design to match stellar design system.
Further work
Although the design was made using pure css with AI for speed, I'll open another PR later migrating it to use the Stellar Design System components